/**
 * OWASP Enterprise Security API (ESAPI)
 *
 * This file is part of the Open Web Application Security Project (OWASP)
 * Enterprise Security API (ESAPI) project. For details, please see
 * <a href="http://www.owasp.org/index.php/ESAPI">http://www.owasp.org/index.php/ESAPI</a>.
 *
 * Copyright (c) 2007 - The OWASP Foundation
 *
 * The ESAPI is published by OWASP under the BSD license. You should read and accept the
 * LICENSE before you use, modify, and/or redistribute this software.
 *
 * @author Jeff Williams <a href="http://www.aspectsecurity.com">Aspect Security</a>
 * @created 2007
 */
package org.owasp.esapi.reference.validation;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.owasp.esapi.ESAPI;
import org.owasp.esapi.Encoder;
import org.owasp.esapi.EncoderConstants;
import org.owasp.esapi.Logger;
import org.owasp.esapi.StringUtilities;
import org.owasp.esapi.errors.ValidationException;
import org.owasp.esapi.util.NullSafe;


/**
 * A validator performs syntax and possibly semantic validation of a single
 * piece of data from an untrusted source.
 *
 * @author Jeff Williams (jeff.williams .at. aspectsecurity.com) <a
 *         href="http://www.aspectsecurity.com">Aspect Security</a>
 * @since June 1, 2007
 * @see org.owasp.esapi.Validator
 *
 * http://en.wikipedia.org/wiki/Whitelist
 */
public class StringValidationRule extends BaseValidationRule {
    private static final Logger LOGGER = ESAPI.getLogger(StringValidationRule.class);
    protected List<Pattern> whitelistPatterns = new ArrayList<Pattern>();
    protected List<Pattern> blacklistPatterns = new ArrayList<Pattern>();
    protected int minLength = 0;
    protected int maxLength = Integer.MAX_VALUE;
    private boolean canonicalizeInput = true;

    public StringValidationRule( String typeName ) {
        super( typeName );
    }

    public StringValidationRule( String typeName, Encoder encoder ) {
        super( typeName, encoder );
    }

    public StringValidationRule( String typeName, Encoder encoder, String whitelistPattern ) {
        super( typeName, encoder );
        addWhitelistPattern( whitelistPattern );
    }

    /**
     * @throws IllegalArgumentException if pattern is null
     */
    public void addWhitelistPattern( String pattern ) {
        if (pattern == null) {
            throw new IllegalArgumentException("Pattern cannot be null");
        }
        try {
            whitelistPatterns.add( Pattern.compile( pattern ) );
        } catch( PatternSyntaxException e ) {
            throw new IllegalArgumentException( "Validation misconfiguration, problem with specified pattern: " + pattern, e );
        }
    }

    /**
     * @throws IllegalArgumentException if p is null
     */
    public void addWhitelistPattern( Pattern p ) {
        if (p == null) {
            throw new IllegalArgumentException("Pattern cannot be null");
        }
        whitelistPatterns.add( p );
    }

    /**
     * @throws IllegalArgumentException if pattern is null
     */
    public void addBlacklistPattern( String pattern ) {
        if (pattern == null) {
            throw new IllegalArgumentException("Pattern cannot be null");
        }
        try {
            blacklistPatterns.add( Pattern.compile( pattern ) );
        } catch( PatternSyntaxException e ) {
            throw new IllegalArgumentException( "Validation misconfiguration, problem with specified pattern: " + pattern, e );
        }
    }

    /**
     * @throws IllegalArgumentException if p is null
     */
    public void addBlacklistPattern( Pattern p ) {
        if (p == null) {
            throw new IllegalArgumentException("Pattern cannot be null");
        }
        blacklistPatterns.add( p );
    }

    public void setMinimumLength( int length ) {
        minLength = length;
    }


    public void setMaximumLength( int length ) {
        maxLength = length;
    }

    public void setCanonicalize(boolean canonicalize) {
        this.canonicalizeInput = canonicalize;
    }

    /**
     * checks input against whitelists.
     * @param context The context to include in exception messages
     * @param input the input to check
     * @param orig A origional input to include in exception
     *    messages. This is not included if it is the same as
     *    input.
     * @return input upon a successful check
     * @throws ValidationException if the check fails.
     */
    private String checkWhitelist(String context, String input, String orig) throws ValidationException
    {
        // check whitelist patterns
        for (Pattern p : whitelistPatterns) {
            if ( !p.matcher(input).matches() ) {
                throw new ValidationException( context + ": Invalid input. Please conform to regex " + p.pattern() + ( maxLength == Integer.MAX_VALUE ? "" : " with a maximum length of " + maxLength ), "Invalid input: context=" + context + ", type(" + getTypeName() + ")=" + p.pattern() + ", input=" + input + (NullSafe.equals(orig,input) ? "" : ", orig=" + orig), context );
            }
        }

        return input;
    }

    /**
     * checks input against whitelists.
     * @param context The context to include in exception messages
     * @param input the input to check
     * @return input upon a successful check
     * @throws ValidationException if the check fails.
     */
    private String checkWhitelist(String context, String input) throws ValidationException
    {
        return checkWhitelist(context, input, input);
    }

    /**
     * checks input against blacklists.
     * @param context The context to include in exception messages
     * @param input the input to check
     * @param orig A origional input to include in exception
     *    messages. This is not included if it is the same as
     *    input.
     * @return input upon a successful check
     * @throws ValidationException if the check fails.
     */
    private String checkBlacklist(String context, String input, String orig) throws ValidationException
    {
        // check blacklist patterns
        for (Pattern p : blacklistPatterns) {
            if ( p.matcher(input).matches() ) {
                throw new ValidationException( context + ": Invalid input. Dangerous input matching " + p.pattern() + " detected.", "Dangerous input: context=" + context + ", type(" + getTypeName() + ")=" + p.pattern() + ", input=" + input + (NullSafe.equals(orig,input) ? "" : ", orig=" + orig), context );
            }
        }

        return input;
    }

    /**
     * checks input against blacklists.
     * @param context The context to include in exception messages
     * @param input the input to check
     * @return input upon a successful check
     * @throws ValidationException if the check fails.
     */
    private String checkBlacklist(String context, String input) throws ValidationException
    {
        return checkBlacklist(context, input, input);
    }

    /**
     * checks input lengths
     * @param context The context to include in exception messages
     * @param input the input to check
     * @param orig A origional input to include in exception
     *    messages. This is not included if it is the same as
     *    input.
     * @return input upon a successful check
     * @throws ValidationException if the check fails.
     */
    private String checkLength(String context, String input, String orig) throws ValidationException
    {
        if (input.length() < minLength) {
            throw new ValidationException( context + ": Invalid input. The minimum length of " + minLength + " characters was not met.", "Input does not meet the minimum length of " + minLength + " by " + (minLength - input.length()) + " characters: context=" + context + ", type=" + getTypeName() + "), input=" + input + (NullSafe.equals(input,orig) ? "" : ", orig=" + orig), context );
        }

        if (input.length() > maxLength) {
            throw new ValidationException( context + ": Invalid input. The maximum length of " + maxLength + " characters was exceeded.", "Input exceeds maximum allowed length of " + maxLength + " by " + (input.length()-maxLength) + " characters: context=" + context + ", type=" + getTypeName() + ", orig=" + orig +", input=" + input, context );
        }

        return input;
    }

    /**
     * checks input lengths
     * @param context The context to include in exception messages
     * @param input the input to check
     * @return input upon a successful check
     * @throws ValidationException if the check fails.
     */
    private String checkLength(String context, String input) throws ValidationException
    {
        return checkLength(context, input, input);
    }

    /**
     * checks input emptiness
     * @param context The context to include in exception messages
     * @param input the input to check
     * @param orig A origional input to include in exception
     *    messages. This is not included if it is the same as
     *    input.
     * @return input upon a successful check
     * @throws ValidationException if the check fails.
     */
    private String checkEmpty(String context, String input, String orig) throws ValidationException
    {
        if(!StringUtilities.isEmpty(input))
            return input;
        if(allowNull)
            return null;
        throw new ValidationException( context + ": Input required.", "Input required: context=" + context + "), input=" + input + (NullSafe.equals(input,orig) ? "" : ", orig=" + orig), context );
    }

    /**
     * checks input emptiness
     * @param context The context to include in exception messages
     * @param input the input to check
     * @return input upon a successful check
     * @throws ValidationException if the check fails.
     */
    private String checkEmpty(String context, String input) throws ValidationException
    {
        return checkEmpty(context, input, input);
    }

    /**
     * {@inheritDoc}
     */
    public String getValid( String context, String input ) throws ValidationException
    {
        String data = null;

        // check for empty/null
        if(checkEmpty(context, input) == null)
            return null;

        // check length
        checkLength(context, input);

        // canonicalize
        if (canonicalizeInput) {
            data = encoder.canonicalize(input);
        } else {
            String message = String.format("Input validation excludes canonicalization.  Context: %s   Input: %s", context, input);
            LOGGER.warning(Logger.SECURITY_AUDIT, message);
            data = input;
        }

        // check whitelist patterns
        checkWhitelist(context, data, input);

        // check blacklist patterns
        checkBlacklist(context, data, input);

        // validation passed
        return data;
    }

    /**
     * {@inheritDoc}
     */
    @Override
        public String sanitize( String context, String input ) {
            return whitelist( input, EncoderConstants.CHAR_ALPHANUMERICS );
        }


}

